#include "StarsInterpreter.h"

#include "model-server/ResultMsgHandler.h"
#include "model-server/SimulationMsgHandler.h"
#include "model-server/RequestMsgHandler.h"
#include "model-server/ExitMsgHandler.h"
#include "model-server/CallbackTypedMsgHandler.h"
#include "model-server/JsonUtility.h"

#include "simodo/shell/document/DocumentAdaptor_interface.h"
#include "simodo/variable/Variable.h"
#include "simodo/variable/json/Serialization.h"

#include <QFileInfo>

#include <fstream>
#include <chrono>

#include <string>
#include <optional>
#include <stdexcept>
#include <sstream>

using namespace simodo::edit::plugin::model_server;
using namespace std;

struct ProcessSettings
{
    QString path;
    QStringList args;
};

inline QStringList SIMULATION_SCRIPT_EXTENTION {".scriptc0", ".sc0"};

static ProcessSettings getProcessSettings(
    shell::Runner_plugin * plugin
    , shell::Access_interface & shell
)
{
    auto path_to_runner_setup
                        = shell.provideDataStorageFolder(plugin);

    simodo::variable::Value runner_setup_value;
    simodo::variable::loadJson(
        path_to_runner_setup.toStdString() + "/runner-setup.json"
        , runner_setup_value
    );
    if (!runner_setup_value.isObject()) return {};

    auto setup_object = runner_setup_value.getObject();
    const auto & server_value = setup_object->find(u"simodo_server");
    if (!server_value.isObject()) return {};

    setup_object = server_value.getObject();
    const auto & server_exec_value  = setup_object->find(u"exec");
    const auto & server_args_value  = setup_object->find(u"args");
    if (
        !server_exec_value.isString()
        || !server_args_value.isArray()
    ) return {};
    
    ProcessSettings settings;
    settings.path = shell.performAllMacroSubstitutions(
        QString::fromStdU16String(server_exec_value.getString())
    );
    auto server_args_array = server_args_value.getArray();

    for(const auto & server_args_value : server_args_array->values())
    {
        if (server_args_value.isString())
        {
            auto arg_string = shell.performAllMacroSubstitutions(
                QString::fromStdU16String(
                    server_args_value.getString()
                )
            );
            settings.args << arg_string;
        }
    }

    return settings;
}

StarsInterpreter::StarsInterpreter(shell::Runner_plugin * plugin, shell::Access_interface & shell)
    : _plugin(plugin)
    , _shell(shell)
{
    _server.setReciever(this);

    _msg_handlers.push_back(
        std::make_shared<model_server::CallbackTypedMsgHandler>(
              "record"
            , [this](QJsonObject obj) -> bool
            {
                if (!_shell.getRunnerManagement())
                {
                    return true;
                }

                auto text = JsonUtility::findString(obj, "value");
                if (!text.has_value())
                {
                    return true;
                }

                QCoreApplication::postEvent(
                    this, new RecordEvent({text.value()}), -2
                );

                return true;
            })
    );
    _msg_handlers.push_back(
        std::make_shared<model_server::SimulationMsgHandler>(
            [this](QString message, shell::MessageSeverity severity)
            {
                QCoreApplication::postEvent(
                    this, new RecordEvent({message, severity}), -2
                );
            }
        )
    );
    _msg_handlers.push_back(
        std::make_shared<model_server::ResultMsgHandler>(shell)
    );
    _msg_handlers.push_back(
        std::make_shared<model_server::RequestMsgHandler>(shell, [this](QJsonObject msg)
        {
            _writeMessage(msg);
        })
    );
    _msg_handlers.push_back(
        std::make_shared<model_server::CallbackTypedMsgHandler>(
              "stop"
            , [this](QJsonObject) -> bool
            {
                stoped_slot();
                return true;
            })
    );
    _msg_handlers.push_back(
        std::make_shared<model_server::ExitMsgHandler>(shell, [this]{ _server.stop(); })
    );

    connect(this, &StarsInterpreter::notification_received_signal, this, &StarsInterpreter::notification_received_slot);
    connect(this, &StarsInterpreter::ran_signal, this, &StarsInterpreter::ran_slot, Qt::QueuedConnection);
    connect(this, &StarsInterpreter::paused_signal, this, &StarsInterpreter::paused_slot, Qt::QueuedConnection);
    connect(this, &StarsInterpreter::stoped_signal, this, &StarsInterpreter::stoped_slot, Qt::QueuedConnection);

    connect( this
           , &StarsInterpreter::shell_send_global_signal
           , this
           , &StarsInterpreter::shell_send_global_slot
           , Qt::QueuedConnection
           );

    _server.moveToThread(&_buffer_thread);
    _buffer_thread.start();
}

StarsInterpreter::~StarsInterpreter()
{
    if (!_server.isRunning())
    {
        return;
    }
    
    _server.stop();

    _buffer_thread.quit();
    _buffer_thread.wait();
}

QString StarsInterpreter::name() const
{
    /// @note Таково "официальное" название системы
    return tr("SIMODO/stars");
}

QString StarsInterpreter::description() const
{
    return tr("SIMODO/stars interpreter");
}

bool StarsInterpreter::isRunnable(const QString & path) 
{
    for (const auto & ext : qAsConst(SIMULATION_SCRIPT_EXTENTION))
    {
        if (path.endsWith(ext)) return QFileInfo(path).exists();
    }

    return false;
}

void StarsInterpreter::shell_send_global_slot(const QString & text, int severity)
{
    _shell.sendGlobal(text, shell::MessageSeverity(severity));
}

bool StarsInterpreter::startModeling(const QString & path) 
{
    _error_text = "";

    if (_state == shell::RunnerState::Paused)
    {
        return pauseModeling();
    }

    if (!isRunnable(path))
    {
        _error_text = tr("The simulation for file '%1' cannot be played").arg(path);
        return false;
    }

    emit ran_signal(path);
    auto adaptor = this->_shell.getDocumentAdaptor(path, true);
    if (adaptor == nullptr)
    {
        emit shell_send_global_signal( QString("Could not open ") + path
                                        , int(shell::MessageSeverity::Error)
                                        );
        emit stoped_signal(path);
        return false;
    }

    QJsonObject jo;
    jo.insert("type", "start");
    jo.insert("path", path);
    jo.insert("script", adaptor->text());

    if (!_writeMessage(jo))
    {
        emit shell_send_global_signal( _error_text
                                        , int(shell::MessageSeverity::Error)
                                        );
        emit stoped_signal(path);
        return false;
    }
    return true;
}

bool StarsInterpreter::pauseModeling() 
{
    QJsonObject jo;
    jo.insert("type", "pause");

    if (!_writeMessage(jo))
    {
        return false;
    }

    if (_state == shell::RunnerState::Running)
    {
        paused_slot();
    }
    else if (_state == shell::RunnerState::Paused)
    {
        ran_slot();
    }

    return true;
}

bool StarsInterpreter::stopModeling() 
{
    QJsonObject jo;
    jo.insert("type", "stop");

    auto result = _writeMessage(jo);
    if (_state == shell::RunnerState::Paused)
    {
        QJsonObject jo;
        jo.insert("type", "pause");

        if (!_writeMessage(jo))
        {
            return false;
        }
    }

    return result;
}

bool StarsInterpreter::send(const QString & data)
{
    QJsonObject jo = QJsonDocument::fromJson(data.toUtf8()).object();
    jo.insert("type", "message");

    return _writeMessage(jo);
}

bool StarsInterpreter::event(QEvent * event)
{
    if (event->type() == JsonIoService::MessageEvent::TYPE)
    {
        auto message_event = static_cast<JsonIoService::MessageEvent *>(event);
        _processServerMessages(message_event->messages);
        return true;
    }

    if (event->type() == RecordEvent::TYPE)
    {
        auto record_event = static_cast<RecordEvent *>(event);
        if (record_event->severity)
        {
            _shell.sendGlobal(record_event->record, *record_event->severity);
        }
        else
        {
            _shell.getRunnerManagement()->received(record_event->record);
        }
        return true;
    }

    return QObject::event(event);
}


void StarsInterpreter::notification_received_slot(const QString text)
{
    if (_shell.getRunnerManagement())
    {
        _shell.getRunnerManagement()->received(text);
    }
}

void StarsInterpreter::ran_slot()
{
    _state = shell::RunnerState::Running;
    if (_shell.getRunnerManagement())
    {
        _shell.getRunnerManagement()->started();
    }
}

void StarsInterpreter::paused_slot()
{
    _state = shell::RunnerState::Paused;
    if (_shell.getRunnerManagement())
    {
        _shell.getRunnerManagement()->paused();
    }
}

void StarsInterpreter::stoped_slot()
{
    _state = shell::RunnerState::Stoped;
    if (_shell.getRunnerManagement())
    {
        _shell.getRunnerManagement()->stoped();
    }
}

void StarsInterpreter::_processServerMessage(QJsonDocument message)
{
    if (message.isEmpty())
    {
        _shell.sendGlobal(
            "Recieved empty message from model server"
            , shell::MessageSeverity::Error
        );
        _server.stop();
        stoped_slot();
        return;
    }

    auto message_object = message.object();

    for (auto handler : _msg_handlers)
    {
        if (handler->handle(message_object))
        {
            return;
        }
    }

    _shell.sendGlobal(
        "Unknown message: " + QString(message.toJson())
        , shell::MessageSeverity::Warning
    );
}

bool StarsInterpreter::_writeMessage(QJsonObject message)
try
{
    if (!_server.isRunning())
    {
        auto settings = getProcessSettings(_plugin, _shell);
        _server.start(settings.path, settings.args);
    }
    _server.write(QJsonDocument(message));
    return true;
}
catch (logic_error & error)
{
    _error_text = error.what();
    _shell.sendGlobal(_error_text, shell::MessageSeverity::Error);
    return false;
}

void StarsInterpreter::_processServerMessages(QVector<QJsonDocument> messages)
{
    if (messages.isEmpty())
    {
        return;
    }

    for (const auto & m : qAsConst(messages))
    {
        _processServerMessage(m);
    }
}

const QEvent::Type StarsInterpreter::RecordEvent::TYPE
    = QEvent::Type(QEvent::registerEventType());

StarsInterpreter::RecordEvent::RecordEvent(
    QString record, std::optional<shell::MessageSeverity> severity
)
    : QEvent(TYPE)
    , record(record)
    , severity(severity)
{}
